{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "940844d8",
   "metadata": {},
   "source": [
    "在minist手写数字上训练vae图像生成模型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "5473f49f",
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch\n",
    "import torchvision as tv\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2e2b6cf6",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 超参数定义\n",
    "from dataclasses import dataclass\n",
    "\n",
    "\n",
    "@dataclass\n",
    "class Config:\n",
    "    # 文件&数据\n",
    "    data_path: str = \"./dataset/mnist\"\n",
    "    save_dir: str = \"./checkpoints\"\n",
    "    save_every: int = 10\n",
    "    transform = tv.transforms.Compose([\n",
    "        tv.transforms.ToTensor(),\n",
    "        # tv.transforms.Normalize((0.5,), (0.5,))\n",
    "    ])\n",
    "    # 训练参数\n",
    "    device: str = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n",
    "    batch_size: int = 256\n",
    "    num_workers: int = 4\n",
    "    lr: float = 1e-4\n",
    "    num_epochs: int = 300\n",
    "    # 模型参数\n",
    "    img_size = 28\n",
    "    latent_dim = 16\n",
    "\n",
    "g_opt = Config()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "36bf4028",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "共有60000张训练图片，10000张测试图片\n"
     ]
    }
   ],
   "source": [
    "# 数据集\n",
    "from torch.utils.data import DataLoader\n",
    "\n",
    "\n",
    "train_dataset = tv.datasets.MNIST(\n",
    "    root=g_opt.data_path,\n",
    "    train=True,\n",
    "    transform=g_opt.transform,\n",
    "    download=True\n",
    ")\n",
    "test_dataset = tv.datasets.MNIST(\n",
    "    root=g_opt.data_path,\n",
    "    train=False,\n",
    "    transform=g_opt.transform,\n",
    "    download=True\n",
    ")\n",
    "print(f\"共有{len(train_dataset)}张训练图片，{len(test_dataset)}张测试图片\")\n",
    "# 数据加载器\n",
    "train_loader = DataLoader(\n",
    "    train_dataset,\n",
    "    batch_size=g_opt.batch_size,\n",
    "    shuffle=True,\n",
    "    num_workers=g_opt.num_workers\n",
    ")\n",
    "val_loader = DataLoader(\n",
    "    test_dataset,\n",
    "    batch_size=g_opt.batch_size,\n",
    "    shuffle=False,\n",
    "    num_workers=g_opt.num_workers\n",
    ")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a6c52e13",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Utility\n",
    "\n",
    "def save_images(tensor, fn, nrow=8, padding=2):\n",
    "    \"\"\"\n",
    "    保存图像\n",
    "    \"\"\"\n",
    "    grid = tv.utils.make_grid(tensor, nrow=nrow, padding=padding)\n",
    "    tv.utils.save_image(grid, fn)\n",
    "\n",
    "# Loss\n",
    "def vae_loss(recon_x, x, mu, logvar):\n",
    "    # x: (batch_size, 1, 28, 28)\n",
    "    # mu: (batch_size, latent_dim)\n",
    "    # logvar: (batch_size, latent_dim)\n",
    "    batch_size = x.size(0)\n",
    "    recon_loss = F.mse_loss(recon_x, x, reduction='sum') / batch_size\n",
    "    kld = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp()) / batch_size\n",
    "\n",
    "    return recon_loss + kld, recon_loss, kld"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "81cbece5",
   "metadata": {},
   "outputs": [],
   "source": [
    "# VAE Model\n",
    "from typing import Tuple\n",
    "\n",
    "\n",
    "class VAE(nn.Module):\n",
    "    def __init__(self, img_size, latent_dim):\n",
    "        super(VAE, self).__init__()\n",
    "        self.img_size = img_size # 28\n",
    "        self.latent_dim = latent_dim\n",
    "        self.encoder = nn.Sequential(\n",
    "            nn.Conv2d(1, 16, kernel_size=4, stride=2, padding=1), # (B, 1, 28, 28) -> (B, 16, 14, 14)\n",
    "            nn.ReLU(),\n",
    "            nn.Conv2d(16, 32, kernel_size=4, stride=2, padding=1), # (B, 16, 14, 14) -> (B, 32, 7, 7)\n",
    "            nn.ReLU(),\n",
    "            nn.Conv2d(32, 64, kernel_size=3, stride=2, padding=1), # (B, 32, 7, 7) -> (B, 64, 4, 4)\n",
    "            nn.ReLU(),\n",
    "        )\n",
    "        self.fc_mu = nn.Linear(64 * 4 * 4, latent_dim)\n",
    "        self.fc_logvar = nn.Linear(64 * 4 * 4, latent_dim)\n",
    "\n",
    "        self.decoder_input = nn.Linear(latent_dim, 64 * 4 * 4)\n",
    "        self.decoder = nn.Sequential(\n",
    "            nn.Unflatten(1, (64, 4, 4)), # h: (batch_size, 64, 4, 4)\n",
    "            nn.ConvTranspose2d(64, 32, kernel_size=3, stride=2, padding=1), # (B, 64, 4, 4) -> (B, 32, 7, 7)\n",
    "            nn.ReLU(),\n",
    "            nn.ConvTranspose2d(32, 16, kernel_size=4, stride=2, padding=1), # (B, 32, 7, 7) -> (B, 16, 14, 14)\n",
    "            nn.ReLU(),\n",
    "            nn.ConvTranspose2d(16, 1, kernel_size=4, stride=2, padding=1), # (B, 16, 14, 14) -> (B, 1, 28, 28)\n",
    "            nn.Sigmoid(),\n",
    "        )\n",
    "    def forward(self, x):\n",
    "        mu, logvar = self.encode(x)\n",
    "        z = self.reparameterize(mu, logvar)\n",
    "        return self.decode(z), mu, logvar\n",
    "    \n",
    "    def encode(self, x) -> Tuple[torch.Tensor, torch.Tensor]:\n",
    "        # x: (batch_size, 1, 28, 28)\n",
    "        h = self.encoder(x) # h: (batch_size, 64, 3, 3)\n",
    "        h = h.view(h.size(0), -1) # h: (batch_size, 64 * 3 * 3)\n",
    "        return self.fc_mu(h), self.fc_logvar(h) # (batch_size, latent_dim)\n",
    "    \n",
    "    def decode(self, z):\n",
    "        # z: (batch_size, latent_dim)\n",
    "        h = self.decoder_input(z) # h: (batch_size, 64 * 3 * 3)\n",
    "        return self.decoder(h) # (batch_size, 1, 28, 28)\n",
    "    \n",
    "    def reparameterize(self, mu, logvar):\n",
    "        # mu: (batch_size, latent_dim)\n",
    "        # logvar: (batch_size, latent_dim)\n",
    "        std = torch.exp(0.5 * logvar)\n",
    "        eps = torch.randn_like(std)\n",
    "        return mu + eps * std\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "21652e20",
   "metadata": {},
   "outputs": [],
   "source": [
    "from tqdm import tqdm\n",
    "\n",
    "def train_epoch(model, train_loader, optimizer, device):\n",
    "    model.train()\n",
    "    train_loss, recon_loss, kld_loss = 0, 0, 0\n",
    "    for x, y in train_loader:\n",
    "        x = x.to(device)\n",
    "        recon_x, mu, logvar = model(x)\n",
    "        loss, recon_loss, kld_loss = vae_loss(recon_x, x, mu, logvar)\n",
    "        optimizer.zero_grad()\n",
    "        loss.backward()\n",
    "        optimizer.step()\n",
    "        train_loss += loss.item()\n",
    "        recon_loss += recon_loss.item()\n",
    "        kld_loss += kld_loss.item()\n",
    "        \n",
    "    return train_loss/len(train_loader), recon_loss/len(train_loader), kld_loss/len(train_loader)\n",
    "\n",
    "def val_epoch(model, val_loader, device):\n",
    "    model.eval()\n",
    "    with torch.no_grad():\n",
    "        val_loss, recon_loss, kld_loss = 0, 0, 0\n",
    "        for x, y in val_loader:\n",
    "            x = x.to(device)\n",
    "            recon_x, mu, logvar = model(x)\n",
    "            loss, recon_loss, kld_loss = vae_loss(recon_x, x, mu, logvar)\n",
    "            val_loss += loss.item()\n",
    "            recon_loss += recon_loss.item()\n",
    "            kld_loss += kld_loss.item()\n",
    "            \n",
    "    return val_loss/len(val_loader), recon_loss/len(val_loader), kld_loss/len(val_loader)\n",
    "\n",
    "def save_epoch(model, test_loader, epoch, device, times=1):\n",
    "    model.eval()\n",
    "    with torch.no_grad():\n",
    "        for x, y in test_loader:\n",
    "            x = x.to(device)\n",
    "            recon_x, mu, logvar = model(x)\n",
    "            save_images(recon_x.cpu(), f\"{g_opt.save_dir}/recon_epoch_{epoch}_{times}.png\")\n",
    "            if times == 0:\n",
    "                break\n",
    "            times -= 1\n",
    "\n",
    "\n",
    "def train(model, train_loader, val_loader, optimizer, device, num_epochs):\n",
    "    model.to(device)\n",
    "    best_loss = float('inf')\n",
    "\n",
    "    epoch_pbar = tqdm(range(num_epochs), desc=\"Epochs\", position=0)\n",
    "\n",
    "    for epoch in epoch_pbar:\n",
    "        train_loss, _, _ = train_epoch(model, train_loader, optimizer, device)\n",
    "        val_loss, recon_loss, kld_loss = val_epoch(model, val_loader, device)\n",
    "        epoch_pbar.set_postfix(\n",
    "            Train=f\"{train_loss:.4f}\",\n",
    "            Test=f\"{val_loss:.4f}\",\n",
    "            Recon=f\"{recon_loss:.3f}\",\n",
    "            KLD=f\"{kld_loss:.3f}\"\n",
    "        )\n",
    "        # print(f\"Epoch {epoch+1}/{num_epochs} TrainLoss: {train_loss:.4f} TestLoss: {val_loss:.4f} ReconLoss: {recon_loss:.3f} KLD: {kld_loss:.3f}\")        \n",
    "        cur_val_loss = val_loss\n",
    "        # 保存最佳模型\n",
    "        if cur_val_loss < best_loss:\n",
    "            best_loss = cur_val_loss\n",
    "            torch.save({\n",
    "                'epoch': epoch,\n",
    "                'model_state_dict': model.state_dict(),\n",
    "                'optimizer_state_dict': optimizer.state_dict(),\n",
    "                'loss': best_loss\n",
    "            }, f\"{g_opt.save_dir}/best.pth\")\n",
    "            \n",
    "        torch.save({\n",
    "            'epoch': epoch,\n",
    "            'model_state_dict': model.state_dict(),\n",
    "            'optimizer_state_dict': optimizer.state_dict(),\n",
    "            'loss': cur_val_loss\n",
    "        }, f\"{g_opt.save_dir}/last.pth\")\n",
    "\n",
    "        # if epoch % g_opt.save_every == 0 or epoch == num_epochs - 1 or epoch == 0:\n",
    "        #     save_epoch(model, val_loader, epoch, device, times=1)\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "f3db9372",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Epochs: 100%|██████████| 300/300 [04:15<00:00,  1.18it/s, KLD=0.651, Recon=0.585, Test=27.6391, Train=27.8458]\n"
     ]
    }
   ],
   "source": [
    "model = VAE(img_size=g_opt.img_size, latent_dim=g_opt.latent_dim)\n",
    "optimizer = torch.optim.Adam(model.parameters(), lr=g_opt.lr)\n",
    "train(model, train_loader, val_loader, optimizer, g_opt.device, g_opt.num_epochs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "e270b081",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "测试集图像重建效果：\n"
     ]
    },
    {
     "data": {
      "image/png": "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fnyK8jz61c20xnDffffJunXr5LvvvkvRd/r0abl69aqI/P3eXb16VaZNm6b6r127JlOmTMnQv8ubrl/V1f+dv/zyi6xbt87YrkOHDnLlyhV577331GPJycnyzjvvGNtl5PNmL4kuUqSIREZGGvsdAHI7rtgCAHKtESNGSO/evWXmzJnyxBNPyDvvvCMtW7aUunXryuDBg6Vq1apy/PhxWbdunRw+fFg2b96s/i46Olp69+4tAwcOlMaNG0tCQoLExMTI9OnTpX79+vLYY4/JjBkzpH///rJhwwapXLmyREdHy08//SSTJ0+W4ODgDI11165dcscdd8h9990ntWvXlnz58snChQvl+PHjcv/996vtGjduLNOmTZOXXnpJIiMjpVSpUtKuXTuPz33//ffLv/71L+nRo4c888wzcv78eZk2bZpUr17duDFSZGSkjBo1Sl588UWJioqSe++9VwIDA+W3336TcuXKyYQJEzyOYcSIERITEyOdO3eW/v37S+PGjeXcuXPyxx9/SHR0tOzfv19CQ0OlS5cu0qJFCxk5cqTs379fateuLQsWLJAzZ85k6D3zps6dO8uCBQukR48e0qlTJ9m3b59Mnz5dateuLUlJSWq77t27S7NmzWTYsGESGxsrNWvWlJiYGElISBAR82p2ej9vtWvXljZt2kjjxo2lRIkSsn79eomOjpYhQ4b49k0AgJws+27IDABA1rte5ua3335L0Xft2jUnIiLCiYiIUCVw9uzZ4zzyyCNOmTJlnPz58zvly5d3Onfu7ERHRxt/Gx8f7wwZMsQpX768U6BAAadChQpOv379nLi4OLXN8ePHnQEDBjihoaFOgQIFnLp166YoVXO93E9qZXxExBkzZozjOI4TFxfnPP30007NmjWdoKAgJyQkxLn11ludL774wvibY8eOOZ06dXKCg4MdEVFldzy9D47jOEuXLnXq1KnjFChQwKlRo4Yze/bsFOV+rvvwww+dhg0bOoGBgU7x4sWd1q1bO8uWLbvhGBzHcRITE51///vfTmRkpFOgQAEnNDTUad68ufPf//7XuXz5svH+9u3b1ylatKgTEhLi9O3b1/n9999vutzPyZMnjW379evnBAUFpXiO1q1bO7fccotqJycnO6+88ooTHh7uBAYGOg0bNnS+/vrrVEslnTx50nnwwQed4OBgJyQkxOnfv7/z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AAAAAFdjYgsAAAAAcLVszbHNbnauiL623L4dtp7rM3/+fKOPvJEb03OsAgMDjT59Db+dr6zfgt5ew798+XIVx8fHG312voj++nZOoJ6/x770DX1/2PlCFSpUULG9r5YsWWK0Dxw4oGL2Xcbp+8HOv2rTpo2K7Xwr3f79+432vn37VOytfWKfq6OiolRsj1tnl07Q85f8Pf9P//fYeVuHDx9W8S233GL06cffk08+afStWLFCxfa5unjx4iq2c7js3Hj9Hhbt27c3+vQcQf34FjG/A/x9/3nDjXKd09rWzuWrU6eOiu33+dixY0Zbz6m0X4/7W6Sf/V79+eefKn7jjTeMvpiYGBXb50r9HhZ22S89T97+XaaXWhQxyy3a9zSZOnWqinfu3Gn0UeLnxi5cuKDijRs3Gn36uVI/x4qY96TRS/+ImO+7fYzqrydi3vPA/j02ZMgQFdslN99++20V25+J7PxdzRVbAAAAAICrMbEFAAAAALgaE1sAAAAAgKvl6hzbsmXLGu2HH35YxXb9xeeffz7NPqRk5/boebR2/pWeN7V161aj748//lDxL7/8YvTt2LFDxXbOgJ2for++nkMmInLu3DkV27Xj9LqNGam77KlGILlgZj5PrVq1jD49j8OuNWrnt+v7DjfHrmPbokULFdt5W3r+zk8//WT0eauesH4MRUZGGn0jR45UsV0HWT++Nm/ebPQlJCR4ZWxuo+c9ioh89tlnKrbzpyMiIlTcrl07o693794qtnPy9P2u51mLiKxcudJo33777SouWrSo0afXqtVrooqYnzvOoxmnv2d2/que2xweHm70hYWFqdg+vrdv32609XN2Tqpt6Tb251u/30RsbKzRpx9v9m8PT3nN+r1I9ONexDzW7edZv3690ffVV1+p2L6vAVKy94l+vwL9vRQx81/vvPNOo0+/v4Rd41u/j4JebzY1es3bevXqGX3677FWrVoZffp9buzvGP3zSo4tAAAAAAAZwMQWAAAAAOBquWopsl0CZuLEiUZbv821fXvs7777LusG5of0ZU0i5nKG5s2bG316KQG7dMePP/6oYnvJk77UwV7yZC990Jew2Utl9KWM9jIrvc9e7uxpmZW+zMtedqK39WVDjuP47fI6+9+l/7v129mLmLe0t49Dffl5as+LzLOXJtqlBXT6Z9i+zb++RNVTqQd7yZy9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",
      "text/plain": [
       "<Figure size 1200x400 with 16 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "best_model = VAE(img_size=g_opt.img_size, latent_dim=g_opt.latent_dim)\n",
    "best_model.load_state_dict(torch.load(f\"{g_opt.save_dir}/best.pth\")['model_state_dict'])\n",
    "best_model.to(g_opt.device)\n",
    "\n",
    "# 展示VAE的生成效果\n",
    "best_model.eval()\n",
    "\n",
    "\n",
    "dataiter = iter(val_loader)\n",
    "images, labels = next(dataiter)\n",
    "images = images.to(g_opt.device)\n",
    "\n",
    "# 使用模型进行重建（不计算梯度）\n",
    "with torch.no_grad():\n",
    "    reconstructed_images, mu, logvar = model(images)\n",
    "\n",
    "# 将张量移动回CPU以便用matplotlib可视化\n",
    "images_cpu = images.cpu()\n",
    "reconstructed_images_cpu = reconstructed_images.cpu()\n",
    "\n",
    "# 定义一个函数来可视化原始图片和重建图片\n",
    "def show_reconstructions(original, reconstructed, n=8):\n",
    "    plt.figure(figsize=(12, 4))\n",
    "    for i in range(n):\n",
    "        # 显示原始图片\n",
    "        ax = plt.subplot(2, n, i + 1)\n",
    "        plt.imshow(original[i].squeeze(), cmap='gray')\n",
    "        ax.get_xaxis().set_visible(False)\n",
    "        ax.get_yaxis().set_visible(False)\n",
    "        if i == n // 2:\n",
    "            ax.set_title('Original Images')\n",
    "        \n",
    "        # 显示重建图片\n",
    "        ax = plt.subplot(2, n, i + 1 + n)\n",
    "        plt.imshow(reconstructed[i].squeeze(), cmap='gray')\n",
    "        ax.get_xaxis().set_visible(False)\n",
    "        ax.get_yaxis().set_visible(False)\n",
    "        if i == n // 2:\n",
    "            ax.set_title('Reconstructed Images')\n",
    "    plt.show()\n",
    "\n",
    "# 展示前8张图片的重建效果\n",
    "print(\"测试集图像重建效果：\")\n",
    "show_reconstructions(images_cpu, reconstructed_images_cpu, n=8)\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
